Kompleksowy przewodnik po pot臋偶nych typach mapowanych i warunkowych w TypeScript, z praktycznymi przyk艂adami i zaawansowanymi zastosowaniami do tworzenia solidnych i bezpiecznych typologicznie aplikacji.
Opanowanie typ贸w mapowanych i warunkowych w TypeScript
TypeScript, b臋d膮cy nadzbiorem JavaScriptu, oferuje pot臋偶ne funkcje do tworzenia solidnych i 艂atwych w utrzymaniu aplikacji. W艣r贸d tych funkcji typy mapowane (Mapped Types) i typy warunkowe (Conditional Types) wyr贸偶niaj膮 si臋 jako niezb臋dne narz臋dzia do zaawansowanej manipulacji typami. Ten przewodnik stanowi kompleksowy przegl膮d tych koncepcji, omawiaj膮c ich sk艂adni臋, praktyczne zastosowania i zaawansowane przypadki u偶ycia. Niezale偶nie od tego, czy jeste艣 do艣wiadczonym programist膮 TypeScript, czy dopiero zaczynasz swoj膮 przygod臋, ten artyku艂 wyposa偶y Ci臋 w wiedz臋 niezb臋dn膮 do efektywnego wykorzystania tych funkcji.
Czym s膮 typy mapowane?
Typy mapowane pozwalaj膮 na tworzenie nowych typ贸w poprzez transformacj臋 ju偶 istniej膮cych. Iteruj膮 one po w艂a艣ciwo艣ciach istniej膮cego typu i stosuj膮 transformacj臋 do ka偶dej z nich. Jest to szczeg贸lnie przydatne do tworzenia wariant贸w istniej膮cych typ贸w, na przyk艂ad poprzez uczynienie wszystkich w艂a艣ciwo艣ci opcjonalnymi lub tylko do odczytu.
Podstawowa sk艂adnia
Sk艂adnia typu mapowanego wygl膮da nast臋puj膮co:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T: Typ wej艣ciowy, kt贸ry chcesz zmapowa膰.K in keyof T: Iteruje po ka偶dym kluczu w typie wej艣ciowymT.keyof Ttworzy uni臋 wszystkich nazw w艂a艣ciwo艣ci wT, aKreprezentuje ka偶dy pojedynczy klucz podczas iteracji.Transformation: Transformacja, kt贸r膮 chcesz zastosowa膰 do ka偶dej w艂a艣ciwo艣ci. Mo偶e to by膰 dodanie modyfikatora (jakreadonlylub?), zmiana typu lub co艣 zupe艂nie innego.
Praktyczne przyk艂ady
Uczynienie w艂a艣ciwo艣ci tylko do odczytu
Za艂贸偶my, 偶e masz interfejs reprezentuj膮cy profil u偶ytkownika:
interface UserProfile {
name: string;
age: number;
email: string;
}
Mo偶esz utworzy膰 nowy typ, w kt贸rym wszystkie w艂a艣ciwo艣ci s膮 tylko do odczytu:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
Teraz ReadOnlyUserProfile b臋dzie mia艂 te same w艂a艣ciwo艣ci co UserProfile, ale wszystkie b臋d膮 tylko do odczytu.
Uczynienie w艂a艣ciwo艣ci opcjonalnymi
Podobnie, mo偶esz uczyni膰 wszystkie w艂a艣ciwo艣ci opcjonalnymi:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile b臋dzie mia艂 wszystkie w艂a艣ciwo艣ci UserProfile, ale ka偶da z nich b臋dzie opcjonalna.
Modyfikowanie typ贸w w艂a艣ciwo艣ci
Mo偶na r贸wnie偶 modyfikowa膰 typ ka偶dej w艂a艣ciwo艣ci. Na przyk艂ad, mo偶na przekszta艂ci膰 wszystkie w艂a艣ciwo艣ci w ci膮gi znak贸w (string):
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
W tym przypadku wszystkie w艂a艣ciwo艣ci w StringifiedUserProfile b臋d膮 typu string.
Czym s膮 typy warunkowe?
Typy warunkowe pozwalaj膮 na definiowanie typ贸w, kt贸re zale偶膮 od pewnego warunku. Daj膮 one mo偶liwo艣膰 wyra偶ania relacji mi臋dzy typami w oparciu o to, czy dany typ spe艂nia okre艣lone ograniczenie. Jest to podobne do operatora tr贸jargumentowego w JavaScripcie, ale dla typ贸w.
Podstawowa sk艂adnia
Sk艂adnia typu warunkowego wygl膮da nast臋puj膮co:
T extends U ? X : Y
T: Sprawdzany typ.U: Typ, kt贸ryTrozszerza (warunek).X: Typ zwracany, je艣liTrozszerzaU(warunek jest prawdziwy).Y: Typ zwracany, je艣liTnie rozszerzaU(warunek jest fa艂szywy).
Praktyczne przyk艂ady
Okre艣lanie, czy typ jest ci膮giem znak贸w (string)
Stw贸rzmy typ, kt贸ry zwraca string, je艣li typ wej艣ciowy jest ci膮giem znak贸w, a w przeciwnym razie number:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
Wyodr臋bnianie typu z unii
Mo偶esz u偶y膰 typ贸w warunkowych, aby wyodr臋bni膰 okre艣lony typ z typu unii. Na przyk艂ad, aby wyodr臋bni膰 typy, kt贸re nie mog膮 by膰 null:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
W tym przypadku, je艣li T to null lub undefined, typ staje si臋 never, kt贸ry jest nast臋pnie odfiltrowywany przez mechanizm upraszczania unii typ贸w w TypeScript.
Wnioskowanie typ贸w
Typy warunkowe mog膮 by膰 r贸wnie偶 u偶ywane do wnioskowania typ贸w za pomoc膮 s艂owa kluczowego infer. Pozwala to na wyodr臋bnienie typu z bardziej z艂o偶onej struktury typ贸w.
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
W tym przyk艂adzie ReturnType wyodr臋bnia typ zwracany przez funkcj臋. Sprawdza, czy T jest funkcj膮, kt贸ra przyjmuje dowolne argumenty i zwraca typ R. Je艣li tak, zwraca R; w przeciwnym razie zwraca any.
艁膮czenie typ贸w mapowanych i warunkowych
Prawdziwa moc typ贸w mapowanych i warunkowych ujawnia si臋 podczas ich 艂膮czenia. Pozwala to tworzy膰 bardzo elastyczne i wyraziste transformacje typ贸w.
Przyk艂ad: G艂臋boki Readonly
Cz臋stym przypadkiem u偶ycia jest stworzenie typu, kt贸ry sprawia, 偶e wszystkie w艂a艣ciwo艣ci obiektu, w tym zagnie偶d偶one, staj膮 si臋 tylko do odczytu. Mo偶na to osi膮gn膮膰 za pomoc膮 rekurencyjnego typu warunkowego.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
W tym miejscu DeepReadonly rekurencyjnie stosuje modyfikator readonly do wszystkich w艂a艣ciwo艣ci i ich zagnie偶d偶onych w艂a艣ciwo艣ci. Je艣li w艂a艣ciwo艣膰 jest obiektem, rekurencyjnie wywo艂uje DeepReadonly na tym obiekcie. W przeciwnym razie po prostu stosuje modyfikator readonly do w艂a艣ciwo艣ci.
Przyk艂ad: Filtrowanie w艂a艣ciwo艣ci wed艂ug typu
Za艂贸偶my, 偶e chcesz stworzy膰 typ, kt贸ry zawiera tylko w艂a艣ciwo艣ci o okre艣lonym typie. Mo偶esz to osi膮gn膮膰, 艂膮cz膮c typy mapowane i warunkowe.
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>;
W tym przyk艂adzie FilterByType iteruje po w艂a艣ciwo艣ciach T i sprawdza, czy typ ka偶dej w艂a艣ciwo艣ci rozszerza U. Je艣li tak, w艂膮cza t臋 w艂a艣ciwo艣膰 do wynikowego typu; w przeciwnym razie wyklucza j膮, mapuj膮c klucz na never. Zwr贸膰 uwag臋 na u偶ycie "as" do ponownego mapowania kluczy. Nast臋pnie u偶ywamy `Omit` i `keyof StringProperties`, aby usun膮膰 w艂a艣ciwo艣ci typu string z oryginalnego interfejsu.
Zaawansowane przypadki u偶ycia i wzorce
Opr贸cz podstawowych przyk艂ad贸w, typy mapowane i warunkowe mog膮 by膰 u偶ywane w bardziej zaawansowanych scenariuszach do tworzenia wysoce konfigurowalnych i bezpiecznych typologicznie aplikacji.
Dystrybutywne typy warunkowe
Typy warunkowe s膮 dystrybutywne, gdy sprawdzany typ jest typem unii. Oznacza to, 偶e warunek jest stosowany do ka偶dego cz艂onka unii indywidualnie, a wyniki s膮 nast臋pnie 艂膮czone w nowy typ unii.
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
W tym przyk艂adzie ToArray jest stosowany do ka偶dego cz艂onka unii string | number indywidualnie, co skutkuje typem string[] | number[]. Gdyby warunek nie by艂 dystrybutywny, wynikiem by艂oby (string | number)[].
U偶ywanie typ贸w u偶ytkowych (Utility Types)
TypeScript dostarcza kilka wbudowanych typ贸w u偶ytkowych, kt贸re wykorzystuj膮 typy mapowane i warunkowe. Te typy u偶ytkowe mog膮 by膰 u偶ywane jako elementy sk艂adowe do tworzenia bardziej z艂o偶onych transformacji typ贸w.
Partial<T>: Sprawia, 偶e wszystkie w艂a艣ciwo艣ciTstaj膮 si臋 opcjonalne.Required<T>: Sprawia, 偶e wszystkie w艂a艣ciwo艣ciTstaj膮 si臋 wymagane.Readonly<T>: Sprawia, 偶e wszystkie w艂a艣ciwo艣ciTstaj膮 si臋 tylko do odczytu.Pick<T, K>: Wybiera zbi贸r w艂a艣ciwo艣ciKz typuT.Omit<T, K>: Usuwa zbi贸r w艂a艣ciwo艣ciKz typuT.Record<K, T>: Tworzy typ ze zbiorem w艂a艣ciwo艣ciKo typieT.Exclude<T, U>: Wyklucza zTwszystkie typy, kt贸re s膮 przypisywalne doU.Extract<T, U>: Wyodr臋bnia zTwszystkie typy, kt贸re s膮 przypisywalne doU.NonNullable<T>: Wykluczanulliundefinedz typuT.Parameters<T>: Pozyskuje parametry typu funkcyjnegoT.ReturnType<T>: Pozyskuje typ zwracany przez typ funkcyjnyT.InstanceType<T>: Pozyskuje typ instancji typu funkcji konstruktoraT.
Te typy u偶ytkowe to pot臋偶ne narz臋dzia, kt贸re mog膮 upro艣ci膰 z艂o偶one manipulacje typami. Na przyk艂ad, mo偶na po艂膮czy膰 Pick i Partial, aby stworzy膰 typ, kt贸ry czyni opcjonalnymi tylko wybrane w艂a艣ciwo艣ci:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
W tym przyk艂adzie OptionalDescriptionProduct ma wszystkie w艂a艣ciwo艣ci Product, ale w艂a艣ciwo艣膰 description jest opcjonalna.
U偶ywanie typ贸w szablon贸w litera艂owych
Typy szablon贸w litera艂owych pozwalaj膮 na tworzenie typ贸w opartych na litera艂ach ci膮g贸w znak贸w. Mog膮 by膰 u偶ywane w po艂膮czeniu z typami mapowanymi i warunkowymi do tworzenia dynamicznych i wyrazistych transformacji typ贸w. Na przyk艂ad, mo偶na stworzy膰 typ, kt贸ry dodaje okre艣lony prefiks do wszystkich nazw w艂a艣ciwo艣ci:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
W tym przyk艂adzie PrefixedSettings b臋dzie mia艂 w艂a艣ciwo艣ci data_apiUrl i data_timeout.
Dobre praktyki i uwagi
- Zachowaj prostot臋: Chocia偶 typy mapowane i warunkowe s膮 pot臋偶ne, mog膮 r贸wnie偶 komplikowa膰 kod. Staraj si臋, aby transformacje typ贸w by艂y jak najprostsze.
- U偶ywaj typ贸w u偶ytkowych: Wykorzystuj wbudowane typy u偶ytkowe TypeScripta, gdy tylko jest to mo偶liwe. S膮 one dobrze przetestowane i mog膮 upro艣ci膰 Tw贸j kod.
- Dokumentuj swoje typy: Jasno dokumentuj transformacje typ贸w, zw艂aszcza je艣li s膮 z艂o偶one. Pomo偶e to innym programistom zrozumie膰 Tw贸j kod.
- Testuj swoje typy: U偶ywaj mechanizmu sprawdzania typ贸w w TypeScript, aby upewni膰 si臋, 偶e Twoje transformacje dzia艂aj膮 zgodnie z oczekiwaniami. Mo偶esz pisa膰 testy jednostkowe, aby zweryfikowa膰 zachowanie swoich typ贸w.
- Zwr贸膰 uwag臋 na wydajno艣膰: Z艂o偶one transformacje typ贸w mog膮 wp艂ywa膰 na wydajno艣膰 kompilatora TypeScript. B膮d藕 艣wiadomy z艂o偶ono艣ci swoich typ贸w i unikaj niepotrzebnych oblicze艅.
Podsumowanie
Typy mapowane i typy warunkowe to pot臋偶ne funkcje w TypeScript, kt贸re umo偶liwiaj膮 tworzenie bardzo elastycznych i wyrazistych transformacji typ贸w. Opanowuj膮c te koncepcje, mo偶esz poprawi膰 bezpiecze艅stwo typ贸w, 艂atwo艣膰 utrzymania i og贸ln膮 jako艣膰 swoich aplikacji TypeScript. Od prostych transformacji, takich jak uczynienie w艂a艣ciwo艣ci opcjonalnymi lub tylko do odczytu, po z艂o偶one rekurencyjne transformacje i logik臋 warunkow膮, te funkcje dostarczaj膮 narz臋dzi potrzebnych do budowania solidnych i skalowalnych aplikacji. Kontynuuj eksploracj臋 i eksperymentowanie z tymi funkcjami, aby w pe艂ni wykorzysta膰 ich potencja艂 i sta膰 si臋 bardziej bieg艂ym programist膮 TypeScript.
Kontynuuj膮c swoj膮 podr贸偶 z TypeScript, pami臋taj o korzystaniu z bogactwa dost臋pnych zasob贸w, w tym oficjalnej dokumentacji TypeScript, spo艂eczno艣ci internetowych i projekt贸w open-source. Wykorzystaj moc typ贸w mapowanych i warunkowych, a b臋dziesz dobrze przygotowany do radzenia sobie z nawet najtrudniejszymi problemami zwi膮zanymi z typami.